--[[
CastYeller2 - Announce important spell events.

Copyright (C) 2009 Adirelle

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
--]]

if not CastYeller2 then return end
local CastYeller2 = CastYeller2 

local L = CastYeller2.L

--------------------------------------------------------------------------------
-- Option definition
--------------------------------------------------------------------------------

local OptionHandler = {}
local AnnounceOptionHandler = {}

local sinkList = {}
local sinkOptions = {}
local sinkAnnounceOptions = {}
local sourceList = {}
local sourceOptions = sinkOptions -- {}
local sourceAnnounceOptions = {}
local customList = {}
local announceGroupList = {}

local newAnnounceName

local announceOptions = {
	handler = AnnounceOptionHandler,
	desc = L['Edit announce specific settings'],
	type = 'group',
	childGroups = 'tab',
	args = {
		general = {
			name = L['General'],
			desc = L['General announce settings'],
			type = 'group',
			order = 10,
			args = {
				enabled = {
					name = L['Enabled'],
					desc = L['Uncheck to completly disable this announce.'],
					type = 'toggle',
					order = 10,
				},
				deleteButton = {
					name = L['Delete'],
					desc = L['Click to delete this announce. This is undoable.'],
					type = 'execute',
					func = 'DeleteAnnounce',
					confirm = function() return L['Do you really want to definitively delete this announce ?'] end,
					order = 20,
				},			
				names = {
					name = L['Names'],
					desc = L['Enter the names of spells, items or auras that can trigger this announce.'],
					type = 'input',
					multiline = true,
					width = 'full',
					set = 'SetNames',
					get = 'GetNames',
					validate = 'ValidateNames',
					order = 30,
				},
				inCombat = {
					name = L['In combat only'],
					desc = L['Check to disable this announce when out of combat.'],
					type = 'toggle',
					order = 35,
				},
			},
		},
		sources = {
			name = L['Sources'],
			type = 'group',
			order = 20,		
			args = {
				enabled = {
					name = L['Sources'],
					desc = L['Select which kind(s) of triggers trigger this announce.'],
					type = 'multiselect',
					arg = 'sources',
					values = sourceList,
					order = 10,
				},			
			},
			plugins = sourceAnnounceOptions,
		},
		sinks = {
			name = L['Outputs'],
			type = 'group',
			order = 30,
			args = {
				enabled = {
					name = L['Outputs'],
					desc = L['Select to which output this announce is sent.'],
					type = 'multiselect',
					arg = 'sinks',
					values = sinkList,
					order = 10,
				},
			},
			plugins = sinkAnnounceOptions,
		},
		custom = {
			name = L['Custom announce templates'],
			desc = L['Allow you to define custom announce templates for each trigger.'],
			type = 'group',
			childGroups = 'select',
			args = customList,
			order = -1,
			hidden = function(info) 
				for k, v in pairs(info.handler:GetDatabase(info).triggers) do
					if v then 
						return false
					end
				end
				return true
			end,
		},
	},
	plugins = {},
}
local announceOptionMeta = { __index = announceOptions }

local dbOptions = LibStub('AceDBOptions-3.0'):GetOptionsTable(CastYeller2.db)
dbOptions.order = -10

local options = {
	name = 'CastYeller2',
	handler = OptionHandler,
	get = 'Get',
	set = 'Set',
	type = 'group',
	args = {
		global = {
			name = L['Global options'],
			desc = L['CastYeller2 global options'],
			type = 'group',
			order = 10,
			args = {
				enabled = {
					name = L['CastYeller2 enabled'],
					desc = L['Uncheck this box to totally disable CastYeller2. If you plan to do this definitibely, you should better disable the addon on character selection screen.'],
					type = 'toggle',
					order = 0,
				},
				conditionals = {
					name = L['Disable all announces ...'],
					desc = L['Check situations where you do want any announces. CastYeller2 is quiet when at least one condition is met.'],
					type = 'multiselect',
					values = {
						arena = L['In arenas'],
						battleground = L['In battlegrounds'],
						groupInstance = L['In group instances'],
						raidInstance = L['In raid instances'],
						outsideInstances = L['Outside any instance'],
						soloing = L['When soloing'],
						resting = L['When resting'],
					},
					order = 10,
				},
				squareBrackets = {
					name = L['Square brackets'],
					desc = L['Check this to surround every announce message by square brackets : << message >>'],
					type = 'toggle',
					order = 20,
				},
				sources = {
					name = L['Enabled sources'],
					desc = L['Uncheck a source to disable all its triggers for all announces.'],
					type = 'multiselect',
					arg = 'modules',
					values = sourceList,
					order = 30,
				},
				sinks = {
					name = L['Enabled outputs'],
					desc = L['Uncheck an output to disable it for all announces.'],
					type = 'multiselect',
					arg = 'modules',
					values = sinkList,
					order = 40,
				},
			},
			plugins = sinkOptions,
		},
		announces = {
			name = L['Announce options'],
			desc = L['Announce specific options.'],
			type = 'group',
			childGroups = 'tree',
			order = 20,
			args = {
				newAnnounce = {
					name = '|cffffffff'.. L['New announce...'] .. '|r',
					desc = L['Create a new announce'],
					type = 'group',
					inline = true,
					order = -1,
					args = {
						name = {
							name = L['Name'],
							desc = L['Enter the name of the announce to create.'],
							type = 'input',
							get = function() return newAnnounceName end,
							set = function(_, value) newAnnounceName = value end,
							order = 10,
						},
						create = {
							name = L['Create'],
							desc = L['Click to create the announce.'],
							type = 'execute',
							func = 'CreateAnnounce',
							disabled = function() return not newAnnounceName end,
							order = 20,
						},
					},
				},
			},
			plugins = { announces = announceGroupList },
		},
		profiles = dbOptions,
	},
}

local function RegisterModuleOptions(module)
	local name = module.moduleName
	local title = module.moduleTitle or name
	
	local moduleOptions, moduleOptionTable
	local hasTriggers = module.triggers and next(module.triggers)
	
	if module.moduleType == 'sink' then
		sinkList[name] = title
		moduleOptionTable = sinkOptions
		moduleOptions = module.options
	elseif module.moduleType == 'source' then
		sourceList[name] = title
		if hasTriggers or module.options then
			moduleOptionTable = sourceOptions
			moduleOptions = module.options or {}
			if hasTriggers then
				moduleOptions.triggers = {
					name = L['Enabled triggers'],
					desc = L['Uncheck a trigger to disable it for all announces.'],
					type = 'multiselect',
					values = module.triggers,
					order = 10,
				}
				for trigger, label in pairs(module.triggers) do
					moduleOptions[trigger.."-template"] = {
						name = L["%s announce"]:format(label),
						desc = L["Enter the default announce template for this trigger."],
						type = 'input',
						arg = { "templates", name, trigger },
						validate = function(info, value) return CastYeller2:ValidateTemplate(value) end,
						width = 'full',
						order = 20,
					}
				end
			end
		end
	end
	
	if moduleOptions then
		if moduleOptions.type ~= 'group' then
			moduleOptions = { type = 'group', args = moduleOptions }
		end
		moduleOptions.name = title
		moduleOptions.hidden = function(info) return not CastYeller2.db.profile.modules[name] end
		moduleOptions.desc = L['Module specific options']
		moduleOptionTable[name] = { [name] = moduleOptions }
	end
	
	if hasTriggers or module.announceOptions then
		local checkTable = (module.moduleType == 'sink') and 'sinks' or 'sources'
		local opts = {
			name = title,
			desc = L['Module specific options'],
			type = 'group',
			inline = true,
			order = 20,
			hidden = function(info) return not info.handler:GetDatabase(info)[checkTable][name] end,
			args = module.announceOptions or {},
		}
		if module.moduleType == 'source' then
			sourceAnnounceOptions[name] = { [name] = opts }
		else
			sinkAnnounceOptions[name] = { [name] = opts }
		end

		if hasTriggers then
			opts.args.triggers = {
				name = L['Enabled triggers'],
				desc = L['Select the triggers of this announce.'],
				type = 'multiselect',
				values = module.triggers,
				order = 10,
			}
			for name, label in pairs(module.triggers) do
				customList[name] = {
					name = function(info)
						local str = label
						if not info.handler:GetDatabase(info).custom[name].enabled then
							str = '|cff777777'..str..'|r'
						end
						return str
					end,
					type = 'group',
					hidden = function(info) return not info.handler:GetDatabase(info).triggers[name] end,
					args = {
						enabled = {
							name = L['Enabled'],
							desc = L['Check to enable the use of theses custom templates.'],
							type = 'toggle',
							arg = {"custom", name, "enabled"},
							order = 10,
						},
						announces = {
							name = L['Templates'],
							desc = L['Enter one or more custom templates on separate lines. A random one will be pickup each time the announce is triggered.'],  
							type = 'input',
							multiline = true,
							arg = {"custom", name, "announces"},
							get = "GetCustoms",
							set = "SetCustoms",
							validate = "ValidateCustoms",
							disabled = function(info) 
								local db = info.handler:GetDatabase(info)
								return not (db.triggers[name] and db.custom[name].enabled)
							end,
							width = 'full',
							order = 20,
						},
					},
				}
			end
		end
	end
end

-- Register existing modules
for name, module in CastYeller2:IterateModules() do
	RegisterModuleOptions(module)
end

-- Callback to handle future modules
CastYeller2.RegisterSignal('CastYeller2_Config', 'OnModuleRegistered', RegisterModuleOptions)

-- Handle announce groups

function AddAnnounceGroup(_, name, announce)
	if announceGroupList[name] then return end
	announceGroupList[name] = setmetatable({
		name = function(info)
			local str = name
			if not info.handler:GetDatabase(info).enabled then
				str = '|cff777777'..str..'|r'
			end
			return str
		end,
		arg = announce,
	}, announceOptionMeta)
end

function RemoveAnnounceGroup(_, name)		
	announceGroupList[name] = nil
end

function ResetAnnounceGroups(_)
	for name in pairs(announceGroupList) do
		RemoveAnnounceGroup(_,name)
	end
	for name, announce in pairs(CastYeller2.db.profile.announces) do
		AddAnnounceGroup(_, name, announce)
	end
end

ResetAnnounceGroups()

CastYeller2.RegisterSignal('CastYeller2_Config', 'OnAnnounceCreated', AddAnnounceGroup)
CastYeller2.RegisterSignal('CastYeller2_Config', 'OnAnnounceDeleted',  RemoveAnnounceGroup)
CastYeller2.RegisterSignal('CastYeller2_Config', 'OnConfigLoaded',  ResetAnnounceGroups)

--------------------------------------------------------------------------------
-- Helpers
--------------------------------------------------------------------------------

local function ResolvePath(self, info)
	local db = self:GetDatabase(info)
	local path = info.arg or info[#info]
	--print('Resolving path', path, 'in', db)
	if type(path) == 'table' then
		for i = 1, #path-1 do
			db = db[path[i]]
		end
		return db, path[#path], path
	elseif type(path) == 'string' then
		return db, path, path
	end
end

local function Get(self, info, subKey)
	local db, key = ResolvePath(self, info)
	local value = db[key]
	if info.type == 'multiselect' then
		return value[subKey]
	elseif info.type == 'color' and type(value) == 'table' then
		return unpack(value)
	end
	return value
end

function Set(self, info, ...)
	local db, key, path = ResolvePath(self, info)
	if info.type == 'multiselect' then
		local subKey, value = ...
		db[key][subKey] = value
	elseif info.type == 'color' then
		local t = db[key]
		if type(t) == 'table' then
			t[1], t[2], t[3], t[4] = ...
		else
			db[key] = { ... }
		end
	else
		db[key] = ...
	end
	if type(path) == 'table' then
		path = table.concat(path, "_")
	end
	CastYeller2:SendSignal('OnConfigChanged', info, path, ...)
end

--------------------------------------------------------------------------------
-- General option handling
--------------------------------------------------------------------------------

function OptionHandler:GetDatabase(info)
	return CastYeller2.db.profile
end

OptionHandler.Get = Get
OptionHandler.Set = Set

-- Riped from AceDB-3.0
local function copyDefaults(dest, src)
	-- this happens if some value in the SV overwrites our default value with a non-table
	--if type(dest) ~= "table" then return end
	for k, v in pairs(src) do
		if k == "*" or k == "**" then
			if type(v) == "table" then
				-- This is a metatable used for table defaults
				local mt = {
					-- This handles the lookup and creation of new subtables
					__index = function(t,k)
							if k == nil then return nil end
							local tbl = {}
							copyDefaults(tbl, v)
							rawset(t, k, tbl)
							return tbl
						end,
				}
				setmetatable(dest, mt)
				-- handle already existing tables in the SV
				for dk, dv in pairs(dest) do
					if not rawget(src, dk) and type(dv) == "table" then
						copyDefaults(dv, v)
					end
				end
			else
				-- Values are not tables, so this is just a simple return
				local mt = {__index = function(t,k) return k~=nil and v or nil end}
				setmetatable(dest, mt)
			end
		elseif type(v) == "table" then
			if not rawget(dest, k) then rawset(dest, k, {}) end
			if type(dest[k]) == "table" then
				copyDefaults(dest[k], v)
				if src['**'] then
					copyDefaults(dest[k], src['**'])
				end
			end
		else
			if rawget(dest, k) == nil then
				rawset(dest, k, v)
			end
		end
	end
end

function OptionHandler:CreateAnnounce(info)
	local announces = CastYeller2.db.profile.announces
	local name = newAnnounceName
	if not rawget(announces, name) then
		announces[name] = {}
		copyDefaults(announces[name], CastYeller2.defaultAnnouncesDB['**'])
		local defaultAnnounce = CastYeller2.defaultAnnouncesDB[name]
		if type(defaultAnnounce) == "table" then
			copyDefaults(announces[name], defaultAnnounce)
		end
		CastYeller2:SendSignal('OnAnnounceCreated', name, newAnnounce)
	end
	newAnnounceName = nil
end

do
	local list = {}
	function OptionHandler:GetAnnounceList()
		wipe(list)
		for name, settings in pairs(CastYeller2.db.profile.announces) do
			if settings then
				list[name] = name
			end
		end
		return list
	end
end

--------------------------------------------------------------------------------
-- Announce option handling
--------------------------------------------------------------------------------

local EMPTY_DB = {}

AnnounceOptionHandler.Get = Get
AnnounceOptionHandler.Set = Set

function AnnounceOptionHandler:GetDatabase(info)
	local name = info[2]
	return CastYeller2.db.profile.announces[name], name
end

function AnnounceOptionHandler:GetDefaultAnnounce(info)
	return CastYeller2.defaultAnnouncesDB[info[2]]
end

function AnnounceOptionHandler:DeleteAnnounce(info)
	local db, name = self:GetDatabase(info)
	local announces = CastYeller2.db.profile.announces
	local old = rawget(announces, name)
	if old then
		if GetDefaultAnnounce(info) then
			-- do not allow the default settings to be revived on next reload
			announces[name] = false 
		else
			announces[name] = nil
		end
		CastYeller2:SendSignal('OnAnnounceDeleted', name, db)
	end
end

do
	local tmp = {}	
	function AnnounceOptionHandler:GetNames(info)
		local values = self:Get(info)
		if values then
			wipe(tmp)
			for name in pairs(values) do
				table.insert(tmp, tostring(name))
			end
			return table.concat(tmp, "\n")
		end
	end
end


local nameCache = setmetatable({}, {__mode = 'kv', __index = function(self, toCheck)
	local itemName = GetItemInfo(toCheck)
	if itemName then
		self[toCheck:upper()] = itemName
		return itemName
	end
	local id = rawget(self, '__index') or 0
	local found
	while not found and id < 100000 do
		id = id + 1		
		local name = GetSpellInfo(id)
		if name then
			local upperName = name:upper()
			self[upperName] = name
			if upperName == toCheck then
				found = name
			end
		end
	end
	self.__index = id
	return found
end})

function AnnounceOptionHandler:SetNames(info, value)
	local values = self:Get(info) or {}
	wipe(values)
	for name in value:gmatch("[^\n]+") do
		name = name:trim():upper()
		if name ~= "" then
			values[nameCache[name]] = true
		end
	end
	local default = self:GetDefaultAnnounce(info)
	if default then
		for name in pairs(default.names) do
			values[name] = values[name] or false
		end
	end
	self:Set(info, values)
end

function AnnounceOptionHandler:ValidateNames(info, value)
	for name in value:gmatch("[^\n]+") do
		name = name:trim()
		if name ~= "" and not nameCache[name:upper()] then
			return L["Unknown spell: %s. Please check you did not include the rank."]:format(name)
		end
	end
	return true
end


function AnnounceOptionHandler:GetCustoms(info)
	local list = self:Get(info)
	return type(list) == "table" and table.concat(list, "\n") or tostring(list)
end

function AnnounceOptionHandler:SetCustoms(info, value)
	local list = self:Get(info) or {}
	wipe(list)
	for line in tostring(value):gmatch("[^\n]+") do
		if line:trim() ~= "" then
			table.insert(list, line)
		end
	end
	self:Set(info, list)
end

function AnnounceOptionHandler:ValidateCustoms(info, value)
	for line in tostring(value):gmatch("[^\n]+") do
		line = line:trim()
		local result = CastYeller2:ValidateTemplate(line)
		if result ~= true then
			return result
		end
	end
	return true
end

-- Register the options
LibStub("AceConfig-3.0"):RegisterOptionsTable('CastYeller2', options)

function CastYeller2:OpenConfigGUI() -- Erase loading method
	LibStub("AceConfigDialog-3.0"):Open('CastYeller2')
end

CastYeller2:OpenConfigGUI()

